Hex Bin Converter - for 1K ZX80
by Jim Waterman, 17 August 2021
===============================

Background
----------

This was partially inspired by the original "Hex Bin Converter", which was a listing for the 16K ZX81 from the March/April 1983 issue of Sinclair Programs:
https://archive.org/details/sinclair-programs-06/page/n18/mode/1up

In the previous January/February 1983 issue, there had been a "Hex Loader" listing for the 1K ZX80, intended to provide some sort of access to machine code for the remaining ZX80 users:
https://archive.org/details/sinclair-programs-05/page/n23/mode/1up

"Machine code programming is something which has not been entered into any great depth with the ZX80."

And I can't say I'm surprised.

It took until late 2020 for me to *finally* take up elementary machine code on the Spectrum; within a month or two I'd tried it on the ZX81 as well, but the ZX80 had a massive barrier in the way. Not its lack of memory, but the way it handled and displayed programs. On the ZX81, machine code was accessible via starting a program with a REM statement and poking the relevant bytes from 16514 onwards - any characters which could not be displayed appeared as a question mark, with no further aggro.

The "Hex Loader" program did warn ZX80 users not to use the number code 118 because it would cause all sorts of havoc. This is the HALT instruction which is pretty much useless on the ZX80, but even so, the value might be useful (as the machine code equivalent of a variable). The problem is with ALL THE VALUES FROM 64 TO 127, some of which will cause the REM statement to appear... not as it should, and others will crash the computer completely.

Thanks to Mark Kinsey on Sinclair ZX World (as opposed to everywhere else I might see him around), he explained the safest way to handle machine code on the ZX80 without resorting to REM statements is to hold it in an array instead. The ZX80 only uses one-dimensional number arrays, and with its integer-only arithmetic, a number array requires two bytes per element. Hence, for 100 bytes of code, 50 array elements will store it, and as ZX80 arrays start at 0 rather than 1, DIM Q(49) will provide an array of the right size.

Mark further explained that as variables are stored after the program listing, the machine code will not be stored at a fixed location, further complicating matters, and the start address will be two more than the value held in the system variable VARS. It also means that absolute addresses among that machine code will break as soon as the program length changes!

But I'd set a challenge for the 2021 comp.sys.sinclair Crap Games Competition to make a 1K ZX80 do something useful with machine code. I intended this to mean that the *entire program* was machine code (so I'd load it, find a REM or a blank screen and RANDOMISE USR some value would execute it) - but I need to do some sort of leading by example, even if the machine code is only part of the routine.


The concept
-----------

The original ZX81 "Hex Bin Converter" can take any decimal, hex or binary input and convert it to the other two bases, all done in BASIC. Keeping it simple - i.e. converting decimal to hex and binary, I knew I could do this more easily in machine code, and started with the Spectrum, seeing as the Spin emulator has a built-in assembler so I can test it as it goes. The value is POKEd into a starting address, which converts it to Z80-readable binary, then original version used BIT instructions to analyse the individual bits of that byte, and LDing a 0 or a 1 into eight separate addresses that could then be PEEKed from BASIC. The hex values were just a case of masking off half of the byte at a time, shunting the bits around if needed, and LDing those to two addresses after adjusting the values so that the character codes were stored and were accessible from BASIC using CHR$ in combination with PEEK.

The code was then cut down as BIT instructions all take two bytes and can't be looped. Instead, AND was used to read the bits using ever-halving values (128, 64, 32...), and this can be done in a loop. This more compact code was then further condensed for the ZX81 version, as the characters A-F follow directly after 0-9 and there was no need to check if we were getting a number or a letter - just add 28. Better still, the machine code and the test program all fit into 1K.

Hence, the ZX80 version should be straightforward - the CHR$ and PEEK functions exist in its BASIC, we know how to set up the machine code, but there is the small problem of needing to poke values into specific addresses, given that we do *not* know the starting address for the machine code while we're writing it!

Fortunately, it is possible to use a REM statement to store the absolute values. The initial decimal value has to be POKEd directly into where it starts, but the output will either be the characters 0-9 or A-F, or CHR$ 0 (space), or CHR$ 1 (which appears as a quotation mark). Nothing in the REM statement will crash the ZX80. Hence, it is possible here to use a combination or REM and array.


The code loader
---------------

This program to set up the machine code is actually longer than the final program to use it! If there's anything that shows how much more efficient machine code is, even on a machine that only needs two bytes per numerical value instead of five, this is it. 

The array was set up using an array Q(21) - which has 22 elements in it, thus 44 bytes - then filling it using LET statements with values that appear to have nothing to do with the code we want. What we're actually doing is setting up two values at a time; examine the accompanying spreadsheet to see how I did it. In short, for two consecutive values U and V that are POKEd into the first two memory locations, Q(0) will contain the value U+(256*V), unless that value is greater than 32767 in which case 65536 will be subtracted from it to produce a value U+(256*V)-65536, which will be negative and between -32768 and -1. I have accounted for this in the spreadsheet.


The main program
----------------

Do not, absolutely DO NOT type RUN, or the machine code will be lost, and the RANDOMISE USR statement will crash the ZX80. The array Q does not appear anywhere in the listing; it was saved on (virtual) tape and will be recalled when LOADed alongside the rest of the program. Type GO TO 5 to run the program, retaining array Q.

Then enter your desired one-byte value, between 0 and 255, and the machine code will convert it into both hex and binary. Two versions of the program are provided, one that's as bare-bones as it's possible to get, the other rather more user-friendly. The .O file for the user-friendly program requires 407 bytes, which is room for the (invisible) machine code and the program itself.

That said, I am not sure that a 1K ZX80 would have enough memory to be able to convert decimal to hex, binary or both if it was only done in BASIC - and even then, the BASIC might just not be able to handle it at all. The ZX81 "Hex Bin Converter" requires the use of character arrays, non-integer arithmetic and string manipulation, all of which are absent from the ZX80, and workarounds are likely to be extremely challenging.


The machine code listing explained, for noobs
---------------------------------------------

Knowing that the starting address will be two more than the value contained in VARS:

VARS+2         ld e,255     <- Store the initial value in E. From BASIC, POKE the value we actually want into VARS+3.
VARS+4         ld hl,16429  <- Set HL to 16429, i.e. the address of the third character in the REM statement.
VARS+7         ld b,8       <- Set up to loop 8 times.
VARS+9         ld d,128     <- D will contain the single bit we want to examine (i.e. 10000000 in binary).
VARS+11 loop2: push bc      <- Store the value of B on the stack.
VARS+12        dec b        <- Alter the value of B so that loop3 runs one fewer time than the current value of loop2.
VARS+13        ld a,e       <- Load initial decimal value into A for "processing".
VARS+14        and d        <- Focus on only the single bit currently held in D.
VARS+15 loop3: rrca         <- Move the relevant bit in A right...
VARS+16        djnz loop3   <- ...until it ends up as bit 0 (hence the value in A is either 0 or 1).
VARS+18        pop bc       <- Get the original loop variable back from the stack.
VARS+19        ld (hl),a    <- Drop the 0 or 1 in A into the address stored in HL, i.e. a character in the REM statement.
VARS+20        inc hl       <- Move HL to the next character in the REM statement.
VARS+21        rr d         <- Move the single bit in D right by 1.
VARS+23        djnz loop2   <- Loop until this has been done 8 times.
VARS+25        ld hl,16427  <- Set up HL to 16427, i.e. the address of the first character in the REM statement.
VARS+28        ld a,e       <- Load initial decimal value into A as before.
VARS+29        and 240      <- Focus on only the highest four bits.
VARS+31        rrca         <- Move these bits right...
VARS+32        rrca         <- ...and again...
VARS+33        rrca         <- ...and again...
VARS+34        rrca         <- ...and a fourth time so the top four bits have become the bottom four bits.
VARS+35        add a,28     <- Transform the resultant value (from 0-15) into the character code for 0-9, A-F.
VARS+37        ld (hl),a    <- Drop this character code into the address that HL points to.
VARS+38        inc hl       <- Move HL to the next address.
VARS+39        ld a,e       <- Load initial decimal value into A as before.
VARS+40        and 15       <- Focus on only the lowest four bits - no need to move them.
VARS+42        add a,28     <- Transform the resultant value (from 0-15) into the character code for 0-9, A-F.
VARS+44        ld (hl),a    <- Drop this character code into the address that HL points to.
VARS+45        ret          <- Finished! Return to BASIC.


The conclusion
--------------

Yes, it *is* possible to get the ZX80 to something in machine code that might actually be considered useful.
